Skip to content

Let Handlers Return Multi Round-Trip input_required Results per SEP-2322 - #481

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:input_required_results
Open

Let Handlers Return Multi Round-Trip input_required Results per SEP-2322#481
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:input_required_results

Conversation

@koic

@koic koic commented Aug 4, 2026

Copy link
Copy Markdown
Member

Motivation and Context

SEP-2322 (modelcontextprotocol/modelcontextprotocol#2322, merged for the 2026-07-28 MCP spec release) replaces server-initiated JSON-RPC requests, which the stateless modern lifecycle (SEP-2575) forbids, with multi round-trip results: a tools/call, prompts/get, or resources/read handler returns resultType: "input_required" with inputRequests (server-assigned keys mapping to elicitation/create, sampling/createMessage, or roots/list request shapes) and/or an opaque requestState, and the client retries the ORIGINAL request with inputResponses under the same keys plus the echoed state. The client-side recognition landed earlier (MCP::ResultType, Client::InputRequiredError); this adds the server side, mirroring the Python SDK's low-level Server (python-sdk#2967/#2986) and the TypeScript SDK's umbrella 2026-07-28 work.

  • New MCP::Server::InputRequiredResult value object: construction validation (at least one of the two fields, embedded methods restricted to the three shapes), to_h wire serialization, and the capability mapping shared with the TypeScript SDK's requiredClientCapabilitiesForInputRequest (url-mode elicitation requires elicitation.url, form otherwise; sampling with tools/toolChoice requires sampling.tools; roots/list requires roots), including the 2025 back-compat rule that a bare elicitation: {} declaration implies form support.
  • Handlers return the object through the existing paths: both branches of call_tool_with_args and call_prompt_template_with_args pass it through instead of calling .to_h, call_tool skips output schema validation and the structured-content fallback for it (output schema validation would otherwise run against a nil structuredContent), and the resources/read dispatch skips the contents wrapping and SEP-2549 cache-hint stamping.
  • One central hook in the dispatch lambda gates and serializes the result, after the cancellation check so cancelled requests stay suppressed: a legacy request (no SEP-2575 envelope) gets an internal error, because pre-2026 clients treat an unknown resultType as a final result; embedded requests exceeding the request's declared client capabilities get -32021 with the full merged requiredCapabilities set.
  • MCP::ServerContext gains input_responses, request_state, and the key-tolerant input_response(key) reader. The retry fields are params-top-level siblings of name/arguments/uri (not _meta), so only handlers that opt in to server_context: can participate, matching the envelope readers' access model. The server holds no memory between rounds: handlers re-run from the start on every retry (deterministic replay, as in the Python SDK).

Sealing of the echoed requestState (it arrives as client-controlled input) and the client-side auto-fulfillment loop follow in the next changes.

Refs #382.

How Has This Been Tested?

New test/mcp/server/input_required_result_test.rb covers construction validation, key normalization and freezing, the wire shape of to_h, the full capability mapping, missing-capability subtraction with symbol/string declarations, and the implied-form back-compat rule.

New tests in test/mcp/server_test.rb drive Server#handle with the modern envelope: issuance wire shape for all three methods, the retry leg exposing input_responses/request_state/ input_response(key) to the handler, the legacy-request internal error, -32021 with the merged requiredCapabilities data, output-schema validation bypass under validate_tool_call_results: true, and resources/read results staying unwrapped without ttlMs/cacheScope stamping even when the server configures cache hints.

Breaking Changes

None. The serialization seams only branch on a return type that previously could not occur, all new keyword arguments default to nil, and requests that carry no inputResponses/requestState behave exactly as before.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

…-2322

## Motivation and Context

SEP-2322 (modelcontextprotocol/modelcontextprotocol#2322, merged for the 2026-07-28 MCP spec release)
replaces server-initiated JSON-RPC requests, which the stateless modern lifecycle (SEP-2575) forbids,
with multi round-trip results: a `tools/call`, `prompts/get`, or `resources/read` handler returns
`resultType: "input_required"` with `inputRequests` (server-assigned keys mapping to `elicitation/create`,
`sampling/createMessage`, or `roots/list` request shapes) and/or an opaque `requestState`,
and the client retries the ORIGINAL request with `inputResponses` under the same keys plus the echoed state.
The client-side recognition landed earlier (`MCP::ResultType`, `Client::InputRequiredError`);
this adds the server side, mirroring the Python SDK's low-level Server (python-sdk#2967/#2986)
and the TypeScript SDK's umbrella 2026-07-28 work.

- New `MCP::Server::InputRequiredResult` value object: construction validation
  (at least one of the two fields, embedded methods restricted to the three shapes), `to_h` wire serialization,
  and the capability mapping shared with the TypeScript SDK's `requiredClientCapabilitiesForInputRequest`
  (url-mode elicitation requires `elicitation.url`, form otherwise; sampling with `tools`/`toolChoice` requires
  `sampling.tools`; `roots/list` requires `roots`), including the 2025 back-compat rule that a bare `elicitation: {}`
  declaration implies form support.
- Handlers return the object through the existing paths: both branches of `call_tool_with_args` and
  `call_prompt_template_with_args` pass it through instead of calling `.to_h`, `call_tool` skips
  output schema validation and the structured-content fallback for it (output schema validation would otherwise
  run against a `nil` `structuredContent`), and the `resources/read` dispatch skips the `contents` wrapping and
  SEP-2549 cache-hint stamping.
- One central hook in the dispatch lambda gates and serializes the result, after the cancellation check
  so cancelled requests stay suppressed: a legacy request (no SEP-2575 envelope) gets an internal error,
  because pre-2026 clients treat an unknown `resultType` as a final result; embedded requests exceeding
  the request's declared client capabilities get `-32021` with the full merged `requiredCapabilities` set.
- `MCP::ServerContext` gains `input_responses`, `request_state`, and the key-tolerant `input_response(key)` reader.
  The retry fields are params-top-level siblings of `name`/`arguments`/`uri` (not `_meta`),
  so only handlers that opt in to `server_context:` can participate, matching the envelope readers' access model.
  The server holds no memory between rounds: handlers re-run from the start on every retry
  (deterministic replay, as in the Python SDK).

Sealing of the echoed `requestState` (it arrives as client-controlled input) and the client-side auto-fulfillment loop
follow in the next changes.

Refs modelcontextprotocol#382.

## How Has This Been Tested?

New `test/mcp/server/input_required_result_test.rb` covers construction validation, key normalization and freezing,
the wire shape of `to_h`, the full capability mapping, missing-capability subtraction with symbol/string declarations,
and the implied-form back-compat rule.

New tests in `test/mcp/server_test.rb` drive `Server#handle` with the modern envelope: issuance wire shape for all three methods,
the retry leg exposing `input_responses`/`request_state`/ `input_response(key)` to the handler, the legacy-request internal error,
`-32021` with the merged `requiredCapabilities` data, output-schema validation bypass under `validate_tool_call_results: true`,
and `resources/read` results staying unwrapped without `ttlMs`/`cacheScope` stamping even when the server configures cache hints.

## Breaking Changes

None. The serialization seams only branch on a return type that previously could not occur, all new keyword arguments default to
`nil`, and requests that carry no `inputResponses`/`requestState` behave exactly as before.
@koic
koic force-pushed the input_required_results branch from 778acd2 to d2e568b Compare August 4, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant